AWS WAF コストの最適化、静的コンテンツの保護とログ記録を除外した利用を試してみた

AWS WAF コストの最適化、静的コンテンツの保護とログ記録を除外した利用を試してみた

AWS WAF を適用する際、静的コンテンツへのリクエストを対象外にすることで、Bot Control ルールの課金や、ログ保存、解析コストを最適化。CloudWatch Logs Insight を活用したログの分析例も紹介します。
Clock Icon2024.11.19

AWS WAF Bot Control マネージドルール、リクエスト数に応じて追加の課金が発生します。

  • Common USD 1.00/検査するリクエスト 100 万件
  • Targeted USD 10.00/検査するリクエスト 100 万件

JPEG、PNG などの画像ファイルといった静的コンテンツと、 APIなどの動的コンテンツを 同一のCloudFrontで配信する環境で、静的コンテンツへのリクエストを Bot Control ルールとログ記録の対象外とすることで、AWS WAF コストの最適化を試みる機会がありましたので、紹介します

利用イメージ

WAF保護対象イメージ

黄緑色の線に示した、S3オリジンの静的コンテンツ(画像)へのリクエストを、BotControlルールと、WAFのログ記録の対象外としました。

WAF設定

CloudFront 用の WAF Web ACL とルールを、CloudFormation を利用して構築しました。

WAF設定

画像リクエストの判定

  • 画像ファイルの拡張子を定義した正規表現セットを用意しました。
ExcludePathList:
    Type: AWS::WAFv2::RegexPatternSet
    Properties:
      Name: !Sub '${AWS::StackName}-ExcludePathList'
      Scope: CLOUDFRONT
      Description: Paths to exclude from Bot Control
      RegularExpressionList:
        - ^.*\.png$
        - ^.*\.svg$
  • UriPath が 画像ファイルの拡張子と一致した場合、リクエストを許可 (Allow)。以降のルール (優先度:Priority 2以上)の評価は、省略する指定としました。
  • 画像ファイルに一致したリクエストに対しラベルを付与しました。
    Rules:
        - Name: !Sub '${AWS::StackName}-ExcludePathList'
            Priority: 1
            Statement:
                RegexPatternSetReferenceStatement:
                    FieldToMatch:
                        UriPath: {}
                    Arn: !GetAtt 'ExcludePathList.Arn'
                    TextTransformations:
                        - Type: LOWERCASE
                          Priority: 0
            Action:
                Allow: {}
            VisibilityConfig:
                CloudWatchMetricsEnabled: false
                MetricName: !Sub '${AWS::StackName}-ExcludePathList'
                SampledRequestsEnabled: false
            RuleLabels:
                - Name: !Sub '${AWS::StackName}-StaticAssetAllowList'

Botルール

  • BotControlRule の保護対象定義した正規表現セットを用意しました。
  BotControlPathList:
    Type: AWS::WAFv2::RegexPatternSet
    Properties:
      Name: !Sub '${AWS::StackName}-BotControlPathList'
      Scope: CLOUDFRONT
      Description: Paths targeted for Bot Control

誤検知を避けるため、ブロックを行わない検出(Count)のみとする、OverrideActionを指定しました。

          OverrideAction:
            Count: {}

レートルール

Botルールで付与されたラベル情報を利用。
今回、CategoryAI に分類されたリクエストをレートルール対象とする設定を試みました。

          Action:
            Block: {}
          Statement:
            RateBasedStatement:
              Limit: 10
              AggregateKeyType: IP
              ScopeDownStatement:
                OrStatement:
                  Statements:
                    - LabelMatchStatement:
                        Scope: LABEL
                        Key: awswaf:managed:aws:bot-control:CategoryAI
                    - LabelMatchStatement:
                        Scope: LABEL
                        Key: awswaf:managed:aws:bot-control:bot:category:ai
          VisibilityConfig:
            CloudWatchMetricsEnabled: true
            MetricName: !Sub '${AWS::StackName}-Ratebased1'
            SampledRequestsEnabled: true

レートルールのしきい値、2024年11月時点で最小は10から設定可能です。

https://dev.classmethod.jp/articles/waf-rate-based-rules-lower-rate-limits-10/

ログ設定

  • WAFログをCloudWatchLogsに保存するように設定しました。
  • 静的コンテンツに該当するラベルを含むリクエストは、ログ記録対象外としました。
      LoggingFilter:
        DefaultBehavior: KEEP
        Filters:
          - Behavior: DROP
            Requirement: MEETS_ALL
            Conditions:
              - LabelNameCondition:
                  LabelName: !Sub 'awswaf:${AWS::AccountId}:webacl:${AWS::StackName}-webacl:${AWS::StackName}-StaticAssetAllowList'

テンプレート全文

AWSTemplateFormatVersion: '2010-09-09'
Description: CloudFront WAF WebACL with Bot Control for specific paths and static asset allowlist

Resources:
  WebACL:
    Type: AWS::WAFv2::WebACL
    Properties:
      Name: !Sub '${AWS::StackName}-webacl'
      DefaultAction:
        Allow: {}
      Description: Bot Control for specific paths and static asset allowlist
      Scope: CLOUDFRONT
      VisibilityConfig:
        CloudWatchMetricsEnabled: true
        MetricName: !Sub '${AWS::StackName}'
        SampledRequestsEnabled: false
      Rules:
        - Name: !Sub '${AWS::StackName}-ExcludePathList'
          Priority: 1
          Statement:
            RegexPatternSetReferenceStatement:
              FieldToMatch:
                UriPath: {}
              Arn: !GetAtt 'ExcludePathList.Arn'
              TextTransformations:
                - Type: LOWERCASE
                  Priority: 0
          Action:
            Allow: {}
          VisibilityConfig:
            CloudWatchMetricsEnabled: false
            MetricName: !Sub '${AWS::StackName}-ExcludePathList'
            SampledRequestsEnabled: false
          RuleLabels:
            - Name: !Sub '${AWS::StackName}-StaticAssetAllowList'

        - Name: !Sub '${AWS::StackName}-AWSManagedRulesBotControlRuleSet'
          Priority: 100
          Statement:
            ManagedRuleGroupStatement:
              VendorName: AWS
              Name: AWSManagedRulesBotControlRuleSet
              Version: 'Version_3.0'
              ScopeDownStatement:
                RegexPatternSetReferenceStatement:
                  FieldToMatch:
                    UriPath: {}
                  Arn: !GetAtt 'BotControlPathList.Arn'
                  TextTransformations:
                    - Type: NONE
                      Priority: 0
              ManagedRuleGroupConfigs:
                - AWSManagedRulesBotControlRuleSet:
                    InspectionLevel: COMMON
                    #InspectionLevel: TARGETED
                    #EnableMachineLearning: true
          OverrideAction:
            Count: {}
          VisibilityConfig:
            CloudWatchMetricsEnabled: true
            MetricName: !Sub '${AWS::StackName}-AWSManagedRulesBotControlRuleSet'
            SampledRequestsEnabled: true

        - Name: !Sub '${AWS::StackName}-Ratebased1'
          Priority: 200
          Action:
            Block: {}
          Statement:
            RateBasedStatement:
              Limit: 10
              AggregateKeyType: IP
              ScopeDownStatement:
                OrStatement:
                  Statements:
                    - LabelMatchStatement:
                        Scope: LABEL
                        Key: awswaf:managed:aws:bot-control:CategoryAI
                    - LabelMatchStatement:
                        Scope: LABEL
                        Key: awswaf:managed:aws:bot-control:bot:category:ai
          VisibilityConfig:
            CloudWatchMetricsEnabled: true
            MetricName: !Sub '${AWS::StackName}-Ratebased1'
            SampledRequestsEnabled: true

  ExcludePathList:
    Type: AWS::WAFv2::RegexPatternSet
    Properties:
      Name: !Sub '${AWS::StackName}-ExcludePathList'
      Scope: CLOUDFRONT
      Description: Paths to exclude from Bot Control
      RegularExpressionList:
        - ^.*\.png$
        - ^.*\.svg$
        - ^.*\.ico$
        - ^.*\.jpg$
        - ^.*\.jpeg$

  BotControlPathList:
    Type: AWS::WAFv2::RegexPatternSet
    Properties:
      Name: !Sub '${AWS::StackName}-BotControlPathList'
      Scope: CLOUDFRONT
      Description: Paths targeted for Bot Control
      RegularExpressionList:
        - ^/articles/.*
        - ^/$

  WAFCloudWatchLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub 'aws-waf-logs-${AWS::StackName}'
      RetentionInDays: 14

  WAFLogConfig:
    Type: AWS::WAFv2::LoggingConfiguration
    Properties:
      ResourceArn: !GetAtt WebACL.Arn
      LogDestinationConfigs:
        - !GetAtt WAFCloudWatchLogGroup.Arn
      RedactedFields:
        - SingleHeader:
            Name: authorization
      LoggingFilter:
        DefaultBehavior: KEEP
        Filters:
          - Behavior: DROP
            Requirement: MEETS_ALL
            Conditions:
              - LabelNameCondition:
                  LabelName: !Sub 'awswaf:${AWS::AccountId}:webacl:${AWS::StackName}-webacl:${AWS::StackName}-StaticAssetAllowList'

Not ScopeDownStatement

Bot ルールの定義で Not ScopeDownStatement を利用することで、特定パスを Bot ルールの除外設定とすることも可能です。ただし、Statement の階層が深くなることで設定ミスや管理の複雑化のリスクが高まるため、今回は除外設定のための WAF ルール費用 (月額 1 ドル) を容認しました。

ルール集約

          Statement:
            ManagedRuleGroupStatement:
              VendorName: AWS
              Name: AWSManagedRulesBotControlRuleSet
              Version: 'Version_3.0'
              ScopeDownStatement:
                AndStatement:
                  Statements:
                    - NotStatement:
                        Statement:
                          RegexPatternSetReferenceStatement:
                            FieldToMatch:
                              UriPath: {}
                            Arn: !GetAtt 'ExcludePathList.Arn'
                            TextTransformations:
                              - Type: LOWERCASE
                                Priority: 0
                    - RegexPatternSetReferenceStatement:
                        FieldToMatch:
                          UriPath: {}
                        Arn: !GetAtt 'BotControlPathList.Arn'
                        TextTransformations:
                          - Type: NONE
                            Priority: 0

効果

Bot Conrolの対象を限定した事で、ルールの評価数を 毎分 1400件 → 400件、およそ30% の規模に抑制できました。

Botルール評価数

月間リクエスト数が 1000 万件発生するサイトの場合、Target Bot Control ルールの課金対象となるリクエスト数を 900 万件から 200 万件に抑制できます。これにより、Bot Control ルールの課金を 90 USD から 20 USD に削減できる可能性があります。

ログ確認

CloudWatch Logs に記録された WAF ログは、CloudWatch Logs Insight を利用して集計・分析できます。静的コンテンツのリクエストをログ記録対象から除外することで、Logs Insight のスキャン費用も抑制できます。

https://dev.classmethod.jp/articles/aws-waf-log-support-s3-and-cloudwatch-logs/

確認例

LogsInsight実行例

  • クエリサンプル
fields @timestamp, @message
| parse @message /"action":"(?<action>[^"]+)"/
| parse @message /"country":"(?<country>[^"]+)"/
| parse @message /"clientIp":"(?<clientip>[^"]+)"/
| parse @message /(?i)"User-Agent","value":"(?<user_agent>[^"]+)"/
| parse @message /\{"timestamp":.*,"labels":(?<labels>.*?),"ja3Fingerprint"/
| filter labels like /awswaf:managed:aws:bot-control:bot/
| filter labels not like /awswaf:managed:aws:bot-control:bot:verified/
| filter labels not like /awswaf:managed:aws:bot-control:bot:name:slackbot/
| filter user_agent not like /Twitterbot/
| filter user_agent not like /Hatena/
| stats count(*) as request_count by action, country,clientip, user_agent, labels
| sort request_count desc
| limit 10000
  • fields @timestamp, @message

    • ログエントリのタイムスタンプとメッセージ全体を選択
  • parse @message /"action":"(?<action>[^"]+)"/

    • メッセージから "action" フィールドの値を抽出し、action という変数に格納
    • 例: ALLOW, BLOCK, COUNT
  • parse @message /"country":"(?<country>[^"]+)"/

    • メッセージから "country" フィールドの値を抽出し、country という変数に格納
    • 例: US, JP, CN
  • parse @message /"clientIp":"(?<clientip>[^"]+)"/

    • メッセージから "clientIp" フィールドを抽出
  • parse @message /(?i)"User-Agent","value":"(?<user_agent>[^"]+)"/

    • メッセージから "User-Agent" の値を抽出し、user_agent という変数に格納
    • (?i) は大文字小文字を区別しないマッチングを行うフラグ
  • parse @message /{"timestamp":.,"labels":(?<labels>.?),"ja3Fingerprint"/

    • メッセージから "labels" フィールド全体を抽出し、labels という変数に格納
  • filter labels like /awswaf:managed:aws:bot-control:targeted:aggregate:coordinated_activity:/

    • "coordinated_activity" に関連するボット制御ラベルを持つログエントリのみをフィルタリング
  • 複数行の filter labels not like ...

    • 認定Bot(verified)、制限対象外とするボットをラベルで除外
  • stats count(*) as request_count by action, country, clientip, user_agent, labels

    • フィルタリングされたレコードを action, country, clientip, user_agent, labels でグループ化し、各グループの出現回数を request_count として計算
  • sort request_count desc

  • 結果を request_count の降順でソート

以下は、Logs Insight で特定のクライアント IP からのアクセスが多いことを確認した例です。

フィールド
action ALLOW
clientip 117.102.xx.xx
country JP
labels [{"name":"awswaf:managed:token:absent"},{"name":"awswaf:managed:aws:bot-control:bot:category:http_library"},{"name":"awswaf:managed:aws:bot-control:bot:unverified"},{"name":"awswaf:managed:aws:bot-control:targeted:aggregate:volumetric:ip:token_absent"},{"name":"awswaf:managed:aws:bot-control:bot:name:python"},{"name":"awswaf:managed:captcha:absent"},{"name":"awswaf:managed:aws:bot-control:CategoryHttpLibrary"},{"name":"awswaf:managed:aws:bot-control:bot:name:python_requests"},{"name":"awswaf:managed:aws:bot-control:signal:non_browser_user_agent"}]
request_count 392
user_agent python-requests/2.32.3

このような分析結果に基づき、特定のラベルを持つリクエストをレートルールの対象にすることで、副作用を抑えたアクセスを制限することができます。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.